EC2 Spot Block を Lambda (Python) から起動してみた
こんにちは、菊池です。
起動時間保証つきで低価格にEC2を利用可能なSpot Blockを、Lambdaから起動することで自動化が可能かなと思い、やってみました。
Spot Block とは
うまく使うことでEC2利用料を劇的に下げることができるスポットインスタンスですが、一方でスポット価格の変動によりいつTerminateされるかわからないというリスクがありました。
Spot Blockはスポットインスタンスをリクエストする際のオプションで、通常のスポットよりか価格は上がるものの、一度起動すれば最大6時間まで(1時間単位)の起動が保証されます。
[アップデート]Amazon EC2の新しいSpotインスタンス 「Spot Block」が発表されました! #reinvent
このSpot Block、処理時間が一定の日時バッチ処理などに使いたいと思ったのですが、現時点でマネジメントコンソールからの操作では毎日の繰り返しやAutoScalingへの登録はできません。
そこで、Lambdaを使ってリクエストを発行することで、外部からのトリガやcronでの実行をできようにします。
処理イメージ
スポットインスタンスの起動は、リクエスト後に入札が行われますのでインスタンスの起動までに数分かかります。そのため、以下の2つのファンクションを5分程度の時間差で実行するようにします。
- スポットリクエストを発行し、リクエストIDをSQSに送信するLambdaファンクション
- SQSからリクエストIDを取得し、入札結果をSNSに送信するLambdaファンクション
- インスタンスの起動に成功した場合には起動したインスタンスIDを、失敗した場合はエラーメッセージを送信
Lambda関数
実行させるLambda関数(Python)です。
スポットリクエスト実行:
#!/usr/bin/env python # -*- coding: utf-8 -*- import boto3 import datetime # Spot request Specification spot_price = "0.096" instance_count = 4 request_type = "one-time" duration_minutes = 60 valid_until = datetime.datetime.now() + datetime.timedelta(minutes = duration_minutes) image_id = "ami-29160d47" security_groups = ["sg-b69b93d2"] incetance_type = "m3.medium" availability_zone = "ap-northeast-1a" subnet_id = "subnet-36fb3640" queue_name = "spot_que" client = boto3.client('ec2') sqs = boto3.resource('sqs') def lambda_handler(event, context): # Request Spot Block response = client.request_spot_instances( SpotPrice = spot_price, InstanceCount = instance_count, Type = request_type, ValidUntil = valid_until, BlockDurationMinutes = block_duration_minutes, LaunchSpecification = { "ImageId" : image_id, "SecurityGroupIds" : security_groups, "InstanceType" : incetance_type, "Placement" : { "AvailabilityZone" : availability_zone }, "SubnetId" : subnet_id } ) request_ids =[] request_body = response[response.keys()[0]] for request_id in request_body: request_ids.append( request_id['SpotInstanceRequestId'] ) # Send Request IDs to SQS queue = sqs.get_queue_by_name(QueueName = queue_name) que_response = queue.send_message( MessageBody = ",".join(request_ids)) return request_ids
入札結果取得・通知:
#!/usr/bin/env python # -*- coding: utf-8 -*- import boto3 sqs = boto3.resource('sqs') queue_name = "spot_que" queue = sqs.get_queue_by_name(QueueName = queue_name) spot_client = boto3.client('ec2') sns = boto3.client('sns') def lambda_handler(event, context): #Receive Spot Request IDs from SQS entries = [] messages = queue.receive_messages() entries.append({ "Id" : messages[0].message_id, "ReceiptHandle" : messages[0].receipt_handle }) receive_msg = messages[0].body request_ids = receive_msg.split(",") if len(entries) != 0: response = queue.delete_messages(Entries = entries) spot_response = spot_client.describe_spot_instance_requests( SpotInstanceRequestIds = request_ids ) instance_ids = [] err_message = [] spot_request = spot_response[spot_response.keys()[0]] for result in spot_request: if 'InstanceId' in result: instance_ids.append( result['InstanceId'] ) else: request_status = result['Status'] err_message.append( {result['SpotInstanceRequestId']:request_status['Message']} ) # Send Result to SNS if len(instance_ids) != 0: request = { 'TopicArn' : "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:Success", 'Message' : str(instance_ids), 'Subject' : "Spot request is fulfilled" } response = boto3.client('sns').publish(**request) if len(err_message) != 0: request = { 'TopicArn' : "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:Failed", 'Message' : str(err_message), 'Subject' : "Spot request is Not fulfilled" } response = boto3.client('sns').publish(**request)
実行結果
1つめのファンクションが実行されることでSpot Blockのリクエストが行われます。
リクエスト直後のマネジメントコンソールにはこのようにpendingのステータスで表示されます。
起動に成功した場合
落札に成功し、インスタンスが起動するとステータスがfulfilledになります。
この状態で2つめのファンクションが実行されることでSNSには起動したインスタンスのIDが通知されます。
['i-013c4f8104eb4268b', 'i-089789f89077a4ef7', 'i-0b132c4f3a324f142', 'i-0cd624f4ecc1414e0']
設定した起動希望時間を経過するとインスタンスはTerminateされリクエストはcancelledの状態となります。
起動に失敗した場合
入札価格が低く、インスタンスが起動できなかった場合、ステータスはprice-too-lowとなります。
この状態で2つめのファンクションが実行されると、SNSにはリクエストIDと落札失敗のメッセージを通知します。
[ {'sir-02wpawaf': 'Your Spot request price of 0.01 is lower than the minimum required Spot request fulfillment price of 0.053.'}, {'sir-02wqankz': 'Your Spot request price of 0.01 is lower than the minimum required Spot request fulfillment price of 0.053.'}, {'sir-02wlpf4l': 'Your Spot request price of 0.01 is lower than the minimum required Spot request fulfillment price of 0.053.'}, {'sir-02wrs4ev': 'Your Spot request price of 0.01 is lower than the minimum required Spot request fulfillment price of 0.053.'} ]
設定した起動希望時間を経過するとリクエストはcancelledの状態となります。
まとめ
2つのLambdaファンクションをcron起動に設定することで、毎日同じ時間に起動する、などが実現可能になります。
結果をSNSに通知しますので、価格高騰でインスタンスを起動できなかった場合に、後続処理としてオンデマンドインスタンスを起動するLambdaを用意しておきカバーすることも可能かと思います。